Skip to content

fix(schema): turn the deserializer's silent sinks into named errors - #161

Merged
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/serde-silent-sinks
Aug 9, 2026
Merged

fix(schema): turn the deserializer's silent sinks into named errors#161
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/serde-silent-sinks

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Round 4, vague B — lot schéma & serde, 9 constats vérifiés.

Chacun est un puits silencieux : une valeur légitime — souvent celle que le CSS ferait écrire — avalée sans un mot, produisant un rendu faux plutôt qu'une erreur. Un LLM ne lit pas la sortie de serde : il pose un champ, rien ne proteste, il en conclut que c'est pris en compte.

Constat Sévérité Ce qui était avalé
AnimatedBackground High Preset inconnu, zones, et colors/gradient_typevidéo entièrement noire, aucun diagnostic.
Edges (padding/margin) High {"padding": {"horizontal": 20}}zéro sur les quatre faces.
border-radius par coin High La graphie kebab — celle de tous ses voisins — → rayon 0.
property d'animation High Nom inconnu ou mal casé → animation totalement inerte.
Schéma exporté Medium background déclaré invalide sur Scene/View : 31 violations sur 6 fichiers — les propres exemples du dépôt, contre le schéma que les générateurs consomment.
width: "max-content" Medium Injoignable → boîte à 0 px.
$ littéral Medium Fatal si et seulement si le document contient un bloc config.
position: "relative" Medium CSS légitime → x/y jetés sans un signal.

Le troisième puits du fond noir

Le brief nommait deux puits et demandait de trouver le troisième en lisant la fonction. Il existe : colors et gradient_type étaient parsés en .ok().unwrap_or_default() même avec un preset correctement orthographié. Un colors absent ou malformé produisait donc un dégradé vide — écran noir — sur un scénario par ailleurs irréprochable.

Trouvaille supplémentaire du même passage : un heropattern bien écrit en forme legacy plate tombait aussi dans le bras fourre-tout et devenait un gradient_shift.

Chaque bras route désormais par un struct typé avec propagation d'erreur.

La classe de bug des enums untagged

width: "max-content" était injoignable parce que le fourre-tout String de Length l'absorbait avant que le variant Keyword ne soit tenté. Question posée en ouvert dans le brief : combien d'autres ?

Les 15 enums untagged du dépôt ont été balayés. Deux étaient touchés : Size (le constat) et LineHeight, où "normal" subissait exactement le même sort puis se résolvait à 0. Les deux sont corrigés. Aucun restant — les autres sont soit correctement ordonnés, soit disjoints par forme JSON.

Deux avertissements plutôt que deux rejets, et pourquoi

$ littéral. Le scan tourne désormais systématiquement — l'incohérence venait de ce qu'il ne s'exécutait que dans la branche config — mais il avertit au lieu de rejeter. Un rejet dur casserait tout document légitime contenant un $ (un prix, un $PATH dans un terminal) dès qu'il gagne un bloc config. Par construction, une variable réellement déclarée ne peut pas survivre à merge_variables sans être résolue : ce que le scan trouve encore est donc, par définition, hors du périmètre déclaré. Le rejet dur pour un override référençant une variable non déclarée — cas sans ambiguïté — est inchangé.

position. "relative" et "static" restent du CSS légitime, et un rejet casserait un usage volontaire (sortir du flux sans coordonnées). Avertissement donc — dédoublonné par valeur : prepare_scene réexécute ce Deserialize sur l'arbre entier une fois par frame, un avertissement non gardé sortirait plus de mille fois sur un rendu de 1200 frames. Chaque valeur fautive distincte garde sa ligne, une seule fois. Un test épingle ce garde.

Ce chemin par frame a une conséquence indépendante de cette PR, ouverte en issue #159 : l'arbre JSON complet est re-désérialisé à chaque image, via un enum untagged de 57 variants, sans aucun cache.

Complète la PR #158 à l'étage du dessous

deny_unknown_fields sur Animation et Keyframe — les clés à l'intérieur d'un keyframes[*], que la vague A n'avait pas le droit de toucher.

Reste ouvert

Contre le schéma complet exporté par la CLI (qui fusionne en plus l'union Component), il reste 6 violations sur 3 fichiers, toutes sans rapport avec background et présentes avant cette PR : un enfant valide au runtime ne matche pas le oneOf de Component. Je vérifie et j'ouvre une issue séparée.

Vérification

cargo test --workspace sur cette branche seule : 21 cibles, 0 échec. clippy -D warnings et fmt --check propres. Les 8 exemples : 7 valident (le huitième est l'issue #157, préexistante à main), et les 8 valident désormais contre le schéma exporté par rustmotion-core, contre 31 violations avant.

Nine confirmed audit findings on the JSON contract. Each is a silent sink: a
legitimate value — usually the one CSS would have you write — swallowed
without a word, producing a wrong render instead of an error. An LLM does not
read serde's output; it sets a field, nothing objects, and it concludes the
field was honoured.

- `AnimatedBackground`'s hand-written `Deserialize` had three sinks, not the
  two the audit named. An unknown `preset` fell through `_ =>` to
  `gradient_shift` with no colors; `zones` parsed with
  `.ok().unwrap_or_default()`; and — the third, found by reading the
  function — `colors` and `gradient_type` did the same *even with a
  correctly spelled preset*. The symptom is the worst available: an entirely
  black video, no diagnostic. A correctly spelled `heropattern` in the flat
  legacy form also fell through to `gradient_shift`. Every branch now routes
  through a typed struct with error propagation.
- `Edges` accepted any object at all: `{"padding": {"horizontal": 20}}`
  deserialized to zero on all four sides. `CssStyle`'s `deny_unknown_fields`
  gave the illusion of protection, but the enum one level down had four
  defaulted fields and no guard of its own.
- `border-radius` per-corner was the only composite in `CssStyle` using
  snake_case. The kebab form every neighbour uses failed, fell to another
  variant, and produced radius 0. Both spellings now work, unknown ones are
  reported.
- `width: "max-content"` was unreachable: `Length`'s own string catch-all
  absorbed it before the `Keyword` variant was tried, so the box collapsed
  to 0. `LineHeight` had the identical defect with `"normal"` — found by
  sweeping the other fifteen untagged enums in the repo, which is now the
  complete list: no catch-all-before-specific ordering remains.
- Animation `property` was a free `String` on three types, so an unknown or
  wrongly-cased name made the animation inert. Now constrained at the schema
  layer only — the solver is untouched — with a did-you-mean when the sole
  difference is the naming convention.
- The exported JSON schema declared `background` invalid on `Scene` and
  `View`, because `deny_unknown_fields` emits `additionalProperties: false`
  and `background` was `schemars(skip)`. The repo's own examples failed
  against the schema the generators consume: 31 violations across 6 files.
- A literal `$` in any string was fatal if and only if the document happened
  to contain a `config` block — a price, or `$PATH` in a terminal, blocked
  by an unrelated key elsewhere. The scan now always runs, and warns rather
  than rejecting: a declared variable can never survive `merge_variables`
  unresolved, so anything the scan still finds is by definition outside the
  declared set. An override naming an undeclared variable stays a hard
  error.
- `PositionMode::Named` accepted any string while only `"absolute"` does
  anything, so `"position": "relative"` — legitimate CSS — dropped `x`/`y`
  in silence. It warns now, deduplicated per distinct value: `prepare_scene`
  re-runs this `Deserialize` over the whole tree once per frame, so an
  unguarded warning would print over a thousand times on a 1200-frame
  render.

Also completes PR #158's hardening at the level below: `deny_unknown_fields`
on `Animation` and `Keyframe`, the keys inside a `keyframes[*]`.

Tests: full workspace green on this branch alone; every example still
validates, and now also validates against the exported schema.
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Aug 9, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 9, 2026
@LeadcodeDev
LeadcodeDev merged commit 4675780 into chantier/audit-remediation Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant